package sources;

import gen.Table;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import com.google.common.base.Strings;
import com.google.common.io.Files;
import com.google.common.io.LineProcessor;
import com.google.common.primitives.Chars;
import com.google.common.primitives.Ints;

import constrain.Constraint;




/**
 * @author Kyniek
 * 
 * 	Źródło umożliwia wczytywanie danych wraz z wagami do nich przypisanymi. 
 * 
 *  Jak powinny być zapisane dane? Każda linia odpowiada pojedynczemu elementowi. Jeśli dane są wyposażone w WAGI to powinny to być wartości numeryczne.
 *  Wartości numeryczne wag oraz same dane powinny być rozmieszczone w kolumnach oddzielonych białymi znakami. 
 *  Kolejność kolumn reprezentujących : wagi <=> wartości nie ma znaczenia o ile jedna kolumna jest złozona z wartości numerycznych a druga z wartości tekstowych.
 *  Jeśli obie kolumny są złożone z wartości numerycznych to zakłada się, że pierwsza kolumna dotyczy wag a druga wartości.
 * 
 */
public class AdvancedFileSource implements ISource
{
	public enum Distribution {NATURAL, //porządek w jakim zostały zaczytane dane 
							RANDOM, //porządek zupełnie losowy
							WEIGHTED_RANDOM}//porządek losowy z uwzględnieniem WAG
	
	private enum ColDistrType {ONE_COL,  //gdy plik posiada tylko jedną kolumnę (kolumny są oddzielone białymi znakami) źródło zachowuje się jak Simplefilesource 
							   KEY_FIRST,//gdy kolumna z wartościami wagowymi jest pierwsza 
							   VAL_FIRST}//gdy kolumna z wartościami tekstowymi jest pierwsza
	
	//zmienna wskazująca na sposób rozkładu kolumn, inicjalnie jest null, wpisywana podczas analizy pierwszej odczytanej linii
	private ColDistrType distType = null;
	
	
	private Distribution distribution = Distribution.NATURAL;
	
	private String id = null;
	
	private List<Constraint> cs;
	
	//typ - informacja dla bazy danych
	private String type;
	
	//nazwa źródła, będąca równocześnie nazwą kolumny w bazie danych
	private String name;
	
	//nazwa pliku z którego mają być zaczytane dane	
	private String fileName;
	
	//nazwa tabeli, z niej jest zaczytywana ilość wierszy do zaczytania
	private Table parent;
	
	private int rowLimit = -1;
	
	private int current = 0;
	
	private int total = -1;
	
	List<Integer> weights = new ArrayList<Integer>();
	
	private Random rndGen = new Random(  (int)(Integer.MAX_VALUE * Math.random())  );
	
	//Lista z wierszami.
	List<String> rows;
	
	//kluczami są offsety (ilość wystąpień sumowana z poprzednimi - wagi), wartościami jest druga kolumna
	Map<Integer, String> valMap = new HashMap<Integer, String>();
	
	//tablica wag używana do zwracania wartości losowych z uwzględnieniem wag.
	int[] weightTab;
	
	/**
	 * Podstawowa wersja konstruktora, Nie podaje się informacji o tabeli-rodzicu ani o limicie wierszy - ilość wierszy wtedy jest ograniczone 
	 * ilością wierszy w pliku
	 * @param fileName nazwa pliku
	 * @param name nazwa źródła a zarazem nazwa kolumny
	 * @param type typ dla urzytku bazy danych
	 */
	public AdvancedFileSource(String fileName, String name, String type, Distribution distribution)
	{		
		this.fileName = fileName;
		this.name = name;
		this.type = type;
		this.distribution = distribution;		
		//final Distribution dist = this.distribution;

		try
		{
			rows = readFile(new File(fileName), rowLimit);
		} catch (IOException e)
		{
			e.printStackTrace();
			rows = new ArrayList<String>();
		}
	}	
	
	/**
	 * Wersja konstruktora przyjmująca dodatkowo Tabelę-rodzica. Dzięki temu można odczytać ilość wierszy które należy zaczytać.
	 * @param fileName nazwa pliku źródłowego
	 * @param name nazwa źródła i zarazem kolumny
	 * @param type typ kolumny w bazie danych
	 * @param parent Tabela-rodzic
	 */
	public AdvancedFileSource(String fileName, String name, String type, Distribution distribution, Table parent)
	{	
		this.fileName = fileName;
		this.name = name;
		this.type = type;
		this.parent = parent;
		this.distribution = distribution;		
		this.rowLimit = this.parent.getSize();
		try
		{
			rows = readFile(new File(fileName), rowLimit);
		} catch (IOException e)
		{
			e.printStackTrace();
			rows = new ArrayList<String>();
		}
	}
	
	/**
	 * Wersja konstruktora przyjmująca jako parametr limit - ilość wierszy do zaczytania.
	 * @param fileName nazwa pliku źródłowego
	 * @param name nazwa źródła a zarazem kolumny
	 * @param type typ dla kolumny w bazie danych
	 * @param rowLimit limit wierszy do zaczytania
	 */
	public AdvancedFileSource(String fileName, String name, String type, Distribution distribution, int rowLimit)
	{		
		this.fileName = fileName;
		this.name = name;
		this.type = type;
		this.rowLimit = rowLimit;
		this.distribution = distribution;
		final int limit = rowLimit;
		try
		{
			rows = readFile(new File(fileName), limit);
		} catch (IOException e)
		{
			e.printStackTrace();
			rows = new ArrayList<String>();
		}
	}
	
	
	/**
	 * Metoda czyta plik parsując dane
	 * @param fl plik do przeczytania
	 * @param limit limit wierszy do przeczytania, jeśli wartość wynosi -1 oznacza to czytanie do końca
	 * @return
	 * @throws IOException
	 */
	private List<String> readFile(File fl, final int limit) throws IOException
	{		
		return Files.readLines(new File(this.fileName),Charset.forName("UTF-8"), new LineProcessor<List<String>>() {

			int count = 0;
		
			
			ArrayList<String> result = create( limit);
			
			private ArrayList<String> create(int lim)
			{
				if(lim > 0)
				{
					return new ArrayList<String>(lim);
				}
				else
				{
					return new ArrayList<String>();
				}
			}			
			
			@Override
			public List<String> getResult()
			{					
				//System.out.println(distType);				
				if(!valMap.isEmpty())
				{
					weightTab = Ints.toArray( weights);
				}				
				return result;
			}

			@Override
			public boolean processLine(String arg0) throws IOException
			{
				parseLine(arg0, valMap, result, weights);				
				
				if(count >= limit && limit > 0)
				{
					return false;
				}
				count++;
				return true;
			}
		});
	}
	
	
	/**
	 * Metoda rozbija linię na klucz (Integer) oraz wartość (String) i wkłada do mapy
	 * @param line linia odczytana z pliku
	 * @param mapa
	 */
	private void parseLine(String line, Map<Integer, String> mapa, List<String> ls, List<Integer> wei)
	{
		if(distType == null)
		{
			distType = determineDistrType(line);
		}	
		if(distType != ColDistrType.ONE_COL)
		{
			String[] tab = line.split("\\s+", 2);
			String key = tab[0];//założenie distType = ColDistrType.KEY_FIRST
			String val = tab[1];			
			if(distType == ColDistrType.VAL_FIRST)
			{
				key = tab[1];
				val = tab[0];
			}
			//System.out.println(key + " : " + val);
			key = key.replaceAll( "[^\\d]", "" );
			try
			{
				ls.add(val);
				int tmpVal = Integer.parseInt(key);
				total += tmpVal;
				wei.add(total);
				mapa.put( total, val);
			}
			catch (Exception e)
			{
				System.err.println(e.getMessage());
				System.err.println("Niespójny plik z danymi, wartość oznaczająca popularność okazała się nie być wartością numeryczną");
			}
		}
		else
		{
			ls.add(line);
		}
		
	}
	
	
	/**
	 * Sprawdza typ organizacji danych. Plik z danymi może :
	 * - zawierać jedną kolumnę
	 * - zawierać 2 kolumny gdzie pierwsza determinuje popularność a druga wartości lub odwrotnie.
	 * @param line linia do przeanalizowania
	 * @return
	 */
	private ColDistrType determineDistrType(String line)
	{
		//najpierw należy podzielić linię na tokeny (z założenia powinny być 2
		String[] tokens = line.split("\\s+", 2);
		
		//oczyszczanie tokenów
		for(int i = 0; i < tokens.length; i++)
		{			
			//tokens[i] = tokens[i].replace("\n", "");
			//tokens[i] = tokens[i].replace("\\65279", "");
			tokens[i] =  tokens[i].replaceAll( "[^\\d]", "" );
			tokens[i] = tokens[i].trim();
			//System.out.println(tokens[i]);
		}
	
		if(tokens.length == 1 )
		{
			return ColDistrType.ONE_COL;
		}
		
		//dwie kolumny, czyli to czego oczekuję
		if(tokens.length == 2)
		{
			//System.out.println("val : |" + tokens[0] + "| res : " + ifInt(tokens[0]));
			//System.out.println("val : |" + tokens[1] + "| res : " + ifInt(tokens[1]));
			if(ifInt(tokens[0]) )
			{
				return ColDistrType.KEY_FIRST;
			}
			else if(ifInt(tokens[1]))
			{
				return ColDistrType.VAL_FIRST;
			}
			else
			{
				//System.out.println("wrong....");
				return ColDistrType.ONE_COL;
			}
		}
		else
		{				
			return ColDistrType.ONE_COL;
		}
	}
	
	
	/**
	 * prosta funkcja narzędziowa, sprawdza czy argument jest wartością numeryczną typu całkowitego
	 * @param str
	 * @return
	 */
	private boolean ifInt(String str)
	{
//		System.out.println("beg...");
//		for(int i = 0; i < str.length(); i++)
//		{
//			System.out.println(str.charAt(i) + " " + (int)str.charAt(i)  );
//		}
//		System.out.println("end...");		
		try
		{
			Integer.parseInt(str);
		}
		catch (Exception e)
		{
			return false;
		}
		return true;
//		  NumberFormat formatter = NumberFormat.getInstance();
//		  ParsePosition pos = new ParsePosition(0);
//		  formatter.parse(str, pos);
//		  return str.length() == pos.getIndex();		
	}
	
	
	@Override
	public void setDataType(String typeName)
	{
		type = typeName;
	}

	@Override
	public String getDataType()
	{
		return type;
	}

	@Override
	public void setName(String name)
	{
		this.name = name;
	}

	@Override
	public String getName()
	{
		return name;
	}

	@Override
	public void setUniqueName(String name)
	{
		this.id = name;
	}

	@Override
	public String getUniqueName()
	{
		return id;
	}

	@Override
	public Object get()
	{
		//w zależności od watości przełącznika <distribution> strategia zwracania danych jest rózna
		//dla NATURAL : zwracane są kolejne wartości, zaczytane dane są traktowane jako lista cykliczna
		//dla RANDOM : zwracana jest losowa wartość z zaczytanego zbioru
		//dla WEIGHTED_RANDOM : zwracana jest wartość losowa z uwzględnieniem wag, propabilistyka ważona
		
		switch (distribution)
		{
		case NATURAL :
			if(current >= rows.size())
			{
				current = 0;
			}
			return rows.get(current++);
		case RANDOM :
			return rows.get(rndGen.nextInt(rows.size()));
		case WEIGHTED_RANDOM :

			int target = rndGen.nextInt(total);
			int indexStart = weightTab.length/2;
			int indexRet = 0;
			int indexBeg = 0;
			int indexEnd = weightTab.length - 1;
			
			while( indexBeg <= indexEnd )
			{
				if(target == weightTab[indexStart])
				{
					break;
				}
				if(target < weightTab[indexStart])//jeśli jest po lewo
				{
					indexEnd = indexStart;
					indexStart = indexStart - (indexEnd - indexBeg )/2;  //2;
					//System.out.println("lewo : " + indexStart + " beg : " + indexBeg + " end : " + indexEnd);
					if(target <= weightTab[indexBeg])
					{
						indexStart = indexBeg;
						break;
					}
					
					if(indexStart == indexEnd)
					{
						break;
					}				
				}
				else if(target > weightTab[indexStart])//jeśli jest po prawo....
				{
					indexBeg = indexStart;
					indexStart = indexStart + (indexEnd - indexBeg)/2;
					//System.out.println("prawo : " + indexStart + " beg : " + indexBeg + " end : " + indexEnd);//prawo : 1 beg : 1 end : 2
					if(target >= weightTab[indexEnd] || indexStart == indexBeg)
					{
						indexStart = indexEnd;
						break;
					}
				}			
			}
			//System.out.println("___wylosowana wartość : " + valMap.get(weightTab[indexStart]));
			return valMap.get(weightTab[indexStart]);		
		}		
		return null;
	}

	@Override
	public Object[] get(int dataSize)
	{
		Object res[] = new Object[dataSize];
		for(int i = 0; i < res.length; i++)
		{
			res[i] = get();
		}
		return res;
	}

	@Override
	public String generateDeclaration()
	{
		// TODO Auto-generated method stub
		return getName() + " " + getDataType();
	}

	@Override
	public void setConstraint(Constraint con)
	{
		cs.clear();
		cs.add(con);
	}

	@Override
	public List<Constraint> getConstraint()
	{
		return cs;
	}

	@Override
	public void setConstraint(List<Constraint> con)
	{
		cs = con;		
	}

	public void setDistribution(Distribution distribution)
	{
		this.distribution = distribution;
	}

}


/**
 * TODO :
 * wybieranie ofsetowe :
 * select tab.* from chd1 tab, chd1 prev where tab.parent_id >= 5 AND prev.id = (tab.id - 1)
AND prev.parent_id < 5
 */
